﻿using Sidvall.Data;
using Sidvall.WorkItemManager.Data;
using Sidvall.WorkItemManager.Net.Services.V1;
using System.Linq;
using System.Threading.Tasks;

namespace Sidvall.WorkItemManager.Workflow
{
    public class WorkManagerTask : IWorkItemProviderManager
    {
        #region Public Members

        public WorkItem ActiveWorkItem { get; private set; }
        public ServerManager ServerManager { get; set; }
        public bool IsSavable { get; set; }
        public long? WorkerId { get; set; }
        public bool CleanLog { get; set; }
        public bool SkipActions { get; set; }


        #region ExecuteAsync

        public async Task ExecuteAsync()
        {
            // Load worker
            if (this.WorkerId == null)
                throw new System.Exception("You have to specify a worker.");
            var criteria = new FetchCriteria<Data.Fields.WorkerField, Data.Relations.WorkerRelation>();
            criteria.IncludedEntities = new RelationCollection<Data.Relations.WorkerRelation>();
            criteria.IncludedEntities.Add(Data.Relations.WorkerRelation.WorkItems);
            criteria.IncludedEntities.Add(Data.Relations.WorkerRelation.WorkItemsWorkItemWorkConditions);
            var worker = await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkerManager.GetItemByPrimaryKeyAsync(this.WorkerId.Value, criteria, null, null).ConfigureAwait(false);
            if (worker == null)
                throw new System.Exception("Can't find specified worker.");
            if (!worker.IsEnabled)
                throw new System.Exception("Worker is disabled.");
            var workItems = (from o in worker.WorkItems.Items
                             where o.IsEnabled == true
                             orderby o.ExecutionOrder
                             select o).ToList();
            var count = workItems.Count;

            // Clean log
            if (this.CleanLog)
            {
                for (int i = 0; i < count; i++)
                {
                    this.ActiveWorkItem = workItems[i];
                    await CleanLogAsync(this.SkipActions).ConfigureAwait(false);
                }
            }

            // Process tasks
            for (int i = 0; i < count; i++)
            {
                this.ActiveWorkItem = workItems[i];
                var resultThreadAction = await ExecuteWorkItemAsync().ConfigureAwait(false);
                if (resultThreadAction == ThreadAction.Abort)
                    break;
                if (resultThreadAction == ThreadAction.ContinueWithNextWorkItem)
                    continue;
            }

            // Process tasks (forced execution)
            for (int i = 0; i < count; i++)
            {
                this.ActiveWorkItem = workItems[i];
                if (!this.ActiveWorkItem.ForceExecution)
                    continue;
                await ExecuteWorkItemAsync().ConfigureAwait(false);
            }
        }

        #endregion
        #region ProcessEventAsync

        public async Task ProcessEventAsync(WorkItemProviderEventArgs args)
        {
            WorkItem workItem;
            bool logOldSensorValue;

            this.ActiveWorkItem.ExecutionCompletionPercentage = args.Provider.ExecutionCompletionPercentage;
            this.ActiveWorkItem.ExecutionContext = args.Provider.ExecutionContext;
            this.ActiveWorkItem.ExecutionContextUpdated = System.DateTime.UtcNow;
            try
            {
                switch (args.EventType)
                {
                    case WorkItemEventType.SensorsChanged:
                        logOldSensorValue = this.ActiveWorkItem.SensorLogDayCount > 0;
                        await SaveWorkItemSensorAsync(this.ActiveWorkItem.ServerConnectionId, this.ActiveWorkItem.WorkItemId.Value, logOldSensorValue, args.Sensors).ConfigureAwait(false);
                        break;
                    default:
                        await SaveWorkItemAsync(this.ActiveWorkItem, true).ConfigureAwait(false);
                        break;
                }
                workItem = await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkItemManager.GetItemByPrimaryKeyAsync(this.ActiveWorkItem.WorkItemId.Value).ConfigureAwait(false);
                if (workItem.IsCanceled)
                    args.Cancel = true;
            }
            catch
            {
                args.Cancel = true;
            }
        }

        #endregion

        #region IWorkItemProviderManager Members

        IWorkItem IWorkItemProviderManager.WorkItem
        {
            get
            {
                return this.ActiveWorkItem;
            }
        }

        #endregion

        #endregion
        #region Factory Members

        #region GetWorkManagerTask

        public static WorkManagerTask GetWorkManagerTask(ServerManager serverManager, bool cleanLog, bool skipActions, long? workerId)
        {
            var item = new WorkManagerTask()
            {
                ServerManager = serverManager,
                WorkerId = workerId,
                CleanLog = cleanLog,
                SkipActions = skipActions,
                ActiveWorkItem = null,
            };
            return item;
        }
        public static WorkManagerTask GetWorkManagerTask(ServerManager serverManager, bool cleanLog, bool skipActions, WorkItem activeWorkItem)
        {
            var item = new WorkManagerTask()
            {
                ServerManager = serverManager,
                WorkerId = activeWorkItem?.WorkerId,
                CleanLog = cleanLog,
                SkipActions = skipActions,
                ActiveWorkItem = activeWorkItem,
            };
            return item;
        }

        #endregion

        #endregion
        #region Private Members

        #region CleanLogAsync

        private async Task CleanLogAsync(bool skipActions)
        {
            CommandCriteria item;

            if (!skipActions)
            {
                item = ParameterManager.GetWorkItemCleanLogCommand(this.ActiveWorkItem.ServerConnectionId, new long[] { this.ActiveWorkItem.WorkItemId.Value },
                    this.ActiveWorkItem.LogDayCount, this.ActiveWorkItem.SensorLogDayCount);
                if (item != null)
                    await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkItemManager.ExecuteAsync(item).ConfigureAwait(false);
            }
        }

        #endregion
        #region ExecuteWorkItemAsync

        private async Task<ThreadAction> ExecuteWorkItemAsync()
        {
            System.Collections.Generic.List<WorkItemWorkCondition> workItemConditions;
            WorkItemWorkCondition workItemCondition;
            WorkItemResult workItemResult;
            ThreadAction resultThreadAction, threadAction, groupThreadAction;
            IWorkItemProvider workItemProvider;
            IConditionProvider conditionProvider;
            string currentGroupId, nextGroupId;
            int iMax;
            bool canExecute, isRetry, endOfGroup;

            if ((this.ActiveWorkItem.RetryTime != null) && (this.ActiveWorkItem.RetryTime.Value <= System.DateTime.UtcNow))
                isRetry = true;
            else
                isRetry = false;
            this.ActiveWorkItem.ExecutionStartTime = System.DateTime.UtcNow;
            this.ActiveWorkItem.WorkItemStatusId = WorkItemStatus.Executing;
            this.ActiveWorkItem.IsCanceled = false;
            workItemResult = new WorkItemResult(WorkItemResultType.None, string.Empty);
            if (this.ActiveWorkItem.WorkItemTypeId == null)
                throw new System.Exception("Type is missing.");
            workItemProvider = this.ServerManager.GetWorkItemProvider(this.ActiveWorkItem);
            if (workItemProvider == null)
                throw new System.Exception("Could not create WorkItemProvider instance.");
            this.IsSavable = false;

            // Check conditions
            if (!this.ActiveWorkItem.ForceExecution)
            {
                workItemConditions = (from o in this.ActiveWorkItem.WorkItemWorkConditions.Items
                                      where (o.IsEnabled == true) && (isRetry == false || o.IsEnabledOnRetry == true)
                                      orderby o.GroupId
                                      select o).ToList();
                threadAction = ThreadAction.Continue;
                groupThreadAction = ThreadAction.Abort;
                iMax = workItemConditions.Count;
                for (int i = 0; i < iMax; i++)
                {
                    workItemCondition = workItemConditions[i];
                    if (!string.IsNullOrWhiteSpace(workItemCondition.GroupId))
                        currentGroupId = workItemCondition.GroupId.Trim();
                    else
                        currentGroupId = string.Empty;
                    if (groupThreadAction != ThreadAction.Continue)
                    {
                        conditionProvider = this.ServerManager.GetConditionProvider(workItemCondition.WorkConditionId);
                        if (conditionProvider == null)
                        {
                            throw new System.Exception("Could not create ConditionProvider instance.");
                        }
                        try
                        {
                            conditionProvider.Context = workItemCondition.ConditionContext;
                            if (!this.SkipActions)
                                canExecute = await conditionProvider.CanExecuteAsync(this.ActiveWorkItem).ConfigureAwait(false);
                            else
                                canExecute = true;
                            if (!canExecute)
                                groupThreadAction = MergeThreadActionToLowest(groupThreadAction, workItemCondition.InvalidAction);
                            else
                                groupThreadAction = ThreadAction.Continue;
                        }
                        catch (System.Exception ex)
                        {
                            workItemResult = new WorkItemResult(WorkItemResultType.Failed, ex.Message);
                            break;
                        }
                    }
                    if (i + 1 < iMax)
                    {
                        if (!string.IsNullOrWhiteSpace(workItemConditions[i + 1].GroupId))
                            nextGroupId = workItemConditions[i + 1].GroupId.Trim();
                        else
                            nextGroupId = string.Empty;
                        if ((string.IsNullOrWhiteSpace(nextGroupId)) || (currentGroupId != nextGroupId))
                            endOfGroup = true;
                        else
                            endOfGroup = false;
                    }
                    else
                        endOfGroup = true;
                    if (endOfGroup)
                    {
                        threadAction = MergeThreadActionToHighest(threadAction, groupThreadAction);
                        groupThreadAction = ThreadAction.Abort;
                        if (threadAction == ThreadAction.Abort)
                            break;
                    }
                }
                resultThreadAction = await ProcessResultAsync(workItemResult, null, this.SkipActions).ConfigureAwait(false);
                if ((!this.SkipActions) && (this.IsSavable))
                    await SaveWorkItemAsync(this.ActiveWorkItem, false).ConfigureAwait(false);
                if ((resultThreadAction == ThreadAction.Abort) || (threadAction == ThreadAction.Abort))
                    return ThreadAction.Abort;
                if ((resultThreadAction == ThreadAction.ContinueWithNextWorkItem) || (threadAction == ThreadAction.ContinueWithNextWorkItem))
                    return ThreadAction.Continue;
            }

            // Execute
            try
            {
                if (this.ActiveWorkItem.ForceExecution)
                {
                    this.ActiveWorkItem.ForceExecution = false;
                    await ResetWorkItemAsync().ConfigureAwait(false);
                }
                if (isRetry)
                    this.ActiveWorkItem.RetryCount += 1;
                else
                    this.ActiveWorkItem.RetryCount = 0;

                // Update status
                if (!this.SkipActions)
                    await SaveWorkItemAsync(this.ActiveWorkItem, false).ConfigureAwait(false);

                // Execute
                workItemProvider.ExecutionContext = this.ActiveWorkItem.ExecutionContext;
                workItemProvider.Manager = this;
                workItemProvider.Context = this.ActiveWorkItem.WorkItemContext;
                await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkItemSensorManager.LoadSensorsAsync(workItemProvider,
                    this.ActiveWorkItem.ServerConnectionId, this.ActiveWorkItem.WorkItemId.Value).ConfigureAwait(false);
                if (!this.SkipActions)
                    workItemResult = await workItemProvider.ExecuteAsync().ConfigureAwait(false);
                else
                    workItemResult = new WorkItemResult(WorkItemResultType.Ok, "Skipped Actions");
            }
            catch (System.Exception ex)
            {
                workItemResult = new WorkItemResult(WorkItemResultType.Failed, Sidvall.SystemContext.Current.SystemExceptionManager.GetMessage(ex));
            }
            resultThreadAction = await ProcessResultAsync(workItemResult, workItemProvider, this.SkipActions).ConfigureAwait(false);
            if ((!this.SkipActions) && (this.IsSavable))
                await SaveWorkItemAsync(this.ActiveWorkItem, true).ConfigureAwait(false);
            if (resultThreadAction == ThreadAction.Abort)
                return ThreadAction.Abort;
            return ThreadAction.Continue;
        }

        #endregion
        #region LogResultAsync

        private async Task LogResultAsync(bool skipActions)
        {
            CommandCriteria item;

            if (!skipActions)
            {
                item = ParameterManager.GetWorkItemLogCommand(this.ActiveWorkItem);
                if (item != null)
                    await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkItemManager.ExecuteAsync(item).ConfigureAwait(false);
            }
        }

        #endregion
        #region MergeThreadActionToHighest

        private static ThreadAction MergeThreadActionToHighest(ThreadAction action1, ThreadAction action2)
        {
            switch (action1)
            {
                case ThreadAction.ContinueWithNextWorkItem:
                    if (action2 == ThreadAction.Abort)
                        return ThreadAction.Abort;
                    return action1;
                case ThreadAction.Continue:
                    return action2;
                default:
                    return action1;
            }
        }

        #endregion
        #region MergeThreadActionToLowest

        private static ThreadAction MergeThreadActionToLowest(ThreadAction action1, ThreadAction action2)
        {
            switch (action1)
            {
                case ThreadAction.ContinueWithNextWorkItem:
                    if (action2 == ThreadAction.Continue)
                        return ThreadAction.Continue;
                    return action1;
                case ThreadAction.Abort:
                    return action2;
                default:
                    return action1;
            }
        }

        #endregion
        #region ProcessResultAsync

        private async Task<ThreadAction> ProcessResultAsync(WorkItemResult workItemResult, IWorkItemProvider workItemProvider, bool skipActions)
        {
            IRetryPolicyProvider retryPolicyProvider;

            if (workItemResult.ResultType != WorkItemResultType.None)
            {
                this.ActiveWorkItem.LastExecutionEndTime = System.DateTime.UtcNow;
                this.ActiveWorkItem.LastExecutionStartTime = this.ActiveWorkItem.ExecutionStartTime;
                this.ActiveWorkItem.LastResultMessage = workItemResult.ResultMessage;
                this.ActiveWorkItem.LastWorkItemResultId = workItemResult.ResultType;
                this.ActiveWorkItem.ExecutionStartTime = null;
                if (workItemProvider != null)
                {
                    this.ActiveWorkItem.ExecutionCompletionPercentage = workItemProvider.ExecutionCompletionPercentage;
                    this.ActiveWorkItem.ExecutionContext = workItemProvider.ExecutionContext;
                    this.ActiveWorkItem.ExecutionContextUpdated = System.DateTime.UtcNow;
                }
                this.IsSavable = true;
            }
            switch (workItemResult.ResultType)
            {
                case WorkItemResultType.Canceled:
                    this.ActiveWorkItem.WorkItemStatusId = WorkItemStatus.Idle;
                    if ((this.ActiveWorkItem.LogModeId & LogModes.LogCanceled) == LogModes.LogCanceled)
                        await LogResultAsync(skipActions).ConfigureAwait(false);
                    return this.ActiveWorkItem.CanceledAction;
                case WorkItemResultType.Failed:
                    this.ActiveWorkItem.WorkItemStatusId = WorkItemStatus.Idle;
                    if (this.ActiveWorkItem.WorkRetryPolicyId != null)
                        retryPolicyProvider = this.ServerManager.GetRetryPolicyProvider(this.ActiveWorkItem.WorkRetryPolicyId.Value);
                    else
                        retryPolicyProvider = null;
                    if (retryPolicyProvider != null)
                    {
                        retryPolicyProvider.Context = this.ActiveWorkItem.WorkRetryPolicyContext;
                        this.ActiveWorkItem.RetryTime = await retryPolicyProvider.GetNextRetryTimeAsync(this.ActiveWorkItem).ConfigureAwait(false);
                    }
                    this.ActiveWorkItem.FailedExecutionCount += 1;
                    if ((this.ActiveWorkItem.LogModeId & LogModes.LogFailed) == LogModes.LogFailed)
                        await LogResultAsync(skipActions).ConfigureAwait(false);
                    return this.ActiveWorkItem.FailedAction;
                case WorkItemResultType.Ok:
                    this.ActiveWorkItem.WorkItemStatusId = WorkItemStatus.Idle;
                    this.ActiveWorkItem.RetryTime = null;
                    this.ActiveWorkItem.RetryCount = 0;
                    this.ActiveWorkItem.FailedExecutionCount = 0;
                    this.ActiveWorkItem.ExecutionCompletionPercentage = 100;
                    if ((this.ActiveWorkItem.LogModeId & LogModes.LogSuccess) == LogModes.LogSuccess)
                        await LogResultAsync(skipActions).ConfigureAwait(false);
                    break;
                default:
                    break;
            }
            return ThreadAction.Continue;
        }

        #endregion
        #region ResetWorkItemAsync

        private async Task<CommandCriteria> ResetWorkItemAsync()
        {
            CommandCriteria item;

            item = ParameterManager.GetWorkItemResetCommand(this.ActiveWorkItem.ServerConnectionId, new long[] { this.ActiveWorkItem.WorkItemId.Value },
                null, false, false, false, null, false, false, null, null);
            if (item == null)
                return null;
            return await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkItemManager.ExecuteAsync(item).ConfigureAwait(false);
        }

        #endregion
        #region SaveWorkItemAsync

        private async Task<CommandCriteria> SaveWorkItemAsync(Sidvall.WorkItemManager.Data.IWorkItem workItem, bool saveExecutionContext)
        {
            var item = ParameterManager.GetWorkItemUpdateCommand(workItem, saveExecutionContext);
            if (item == null)
                return null;
            this.IsSavable = false;
            return await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkItemManager.ExecuteAsync(item).ConfigureAwait(false);
        }

        #endregion
        #region SaveWorkItemSensorAsync

        private async Task<CommandCriteria> SaveWorkItemSensorAsync(int? serverConnectionId, long workItemId, bool logOldValue, SensorValueCollection sensors)
        {
            CommandCriteria item;

            item = ParameterManager.GetWorkItemSensorsUpdateCommand(serverConnectionId, workItemId, logOldValue, sensors);
            if (item == null)
                return null;
            return await Sidvall.WorkItemManager.SystemContext.Current.DataPortal.WorkItemSensorManager.ExecuteAsync(item).ConfigureAwait(false);
        }

        #endregion

        #endregion
        #region Constructors

        private WorkManagerTask()
        {
        }

        #endregion
    }
}
